{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "783725f0-857b-4ca6-800a-5837766c8ddc",
   "metadata": {},
   "source": [
    "# CW-Husky SAD Stress Test\n",
    "\n",
    "Checks that:\n",
    "1. The FPGA doesn't overheat when SAD is run continuously at its maximum clock frequency;\n",
    "2. SAD captures are successful at the maximum clock frequency.\n",
    "\n",
    "(1) doesn't need any specific target (or any target at all, really).\n",
    "\n",
    "(2) needs a specific target (SAM4S with the firmware that we program here) in order for the SAD captures to work out-of-the-box, without any tweaking.\n",
    "\n",
    "**If at any time the connection with Husky is \"lost\", the test has failed, and this needs to be investigated.**\n",
    "\n",
    "**If any `scope.XADC` errors occur, they need to be investigated.**"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "0ab4a5eb-f9d6-411c-9d56-344fb8599a8e",
   "metadata": {},
   "outputs": [],
   "source": [
    "PLATFORM = 'CW308_SAM4S'\n",
    "SS_VER = \"SS_VER_1_1\""
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cca0c123-f22e-4a85-be4e-edaa0db8bdbe",
   "metadata": {},
   "outputs": [],
   "source": [
    "import chipwhisperer as cw\n",
    "scope = cw.scope()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "b14f0be1-c93f-4bf3-aad1-fcfbd13d7953",
   "metadata": {},
   "outputs": [],
   "source": [
    "if scope._is_husky_plus:\n",
    "    MAXFREQ = 250e6\n",
    "else:\n",
    "    MAXFREQ = 200e6"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "2b1a17b8-590c-499f-a6f8-c195cded9dda",
   "metadata": {
    "tags": []
   },
   "outputs": [],
   "source": [
    "%run ../jupyter/Setup_Scripts/Setup_Generic.ipynb"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "df58cf19-0a47-4018-ae67-0c3a0ff6a3f8",
   "metadata": {},
   "outputs": [],
   "source": [
    "scope.default_setup()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "39f83286-3c95-4943-850a-0bf1ce6276d6",
   "metadata": {},
   "outputs": [],
   "source": [
    "cw.program_target(scope, prog, \"../../firmware/mcu/simpleserial-trace/simpleserial-trace-{}.hex\".format(PLATFORM))\n",
    "reset_target(scope)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "eacaf0ba-3048-4496-b723-68ee24ac31fc",
   "metadata": {},
   "source": [
    "Change target clock to 10 MHz so that we can hit `MAXFREQ` on the nose:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "4a178f7a-f06e-4051-9c62-15689c8378a6",
   "metadata": {},
   "outputs": [],
   "source": [
    "scope.clock.clkgen_freq = 10e6\n",
    "reset_target(scope)\n",
    "target.baud = 38400 * 10/7.37"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "58d61232-eb02-4d14-b515-4683aa99398b",
   "metadata": {},
   "source": [
    "Check target is alive:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "3781cfc4-725f-4b98-8232-34786fde78d7",
   "metadata": {},
   "outputs": [],
   "source": [
    "scope.trigger.module = 'basic'\n",
    "scope.trigger.triggers = 'tio4'\n",
    "\n",
    "# these are not the errors we care about in this test:\n",
    "scope.adc.lo_gain_errors_disabled = True\n",
    "scope.adc.clip_errors_disabled = True\n",
    "\n",
    "scope.adc.samples = 35000\n",
    "scope.adc.presamples = 0\n",
    "scope.adc.segments = 1\n",
    "scope.adc.bits_per_sample = 8  # SAD is done at 8 bits per sample\n",
    "\n",
    "scope.gain.db = 10\n",
    "\n",
    "reftrace = cw.capture_trace(scope, target, bytearray(16), bytearray(16), as_int=True)\n",
    "assert scope.adc.trig_count == 31864, \"Unexpected trigger count. Are you running the correct firmware?\""
   ]
  },
  {
   "cell_type": "markdown",
   "id": "290f848f-7a5d-4e0a-a7a6-319c1b1c379a",
   "metadata": {},
   "source": [
    "Crank up the ADC clock to its maximum allowed value:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ebfe4cfe-ad47-4acc-a48c-2f26392189e5",
   "metadata": {},
   "outputs": [],
   "source": [
    "scope.clock.adc_mul = int(MAXFREQ / scope.clock.clkgen_freq)\n",
    "reset_target(scope)\n",
    "assert abs(scope.clock.adc_freq - MAXFREQ)/scope.clock.adc_freq < 0.01"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "79692019-15ee-4010-b1bb-0d0308893606",
   "metadata": {},
   "source": [
    "Check that basic capture still works:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "7519a2f3-3e2e-40fd-ab88-457fe767dbd3",
   "metadata": {},
   "outputs": [],
   "source": [
    "reftrace = cw.capture_trace(scope, target, bytearray(16), bytearray(16), as_int=True)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "632a2926-d0eb-4f0f-a0db-359d0135f92f",
   "metadata": {},
   "source": [
    "Set up SAD:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "f3bae998-1141-40d3-add4-99d40696f3a4",
   "metadata": {},
   "outputs": [],
   "source": [
    "refstart = 1000\n",
    "scope.SAD.reference = reftrace.wave[refstart:]\n",
    "scope.SAD.threshold = 20\n",
    "scope.SAD.interval_threshold = 20\n",
    "scope.SAD.multiple_triggers = True\n",
    "scope.SAD.emode = False\n",
    "scope.SAD.always_armed = False\n",
    "\n",
    "scope.trigger.module = 'SAD'"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f832aa8e-e40b-4bfa-a2ff-0c02429e1884",
   "metadata": {},
   "source": [
    "# 🔥 Let's get hot 🔥\n",
    "\n",
    "In this first test we set `scope.SAD.always_armed`, which turns on all the SAD logic (even if we're not actively trying to trigger). No target is required for this.\n",
    "\n",
    "We continuously poll the FPGA temperature (and plot it); we periodically do a least squares linear regression on the last minute of temperature data; we stop when the slope is close enough to flat.\n",
    "\n",
    "This takes a few minutes."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "164110a6-43d7-47ed-8787-378964d5d393",
   "metadata": {},
   "outputs": [],
   "source": [
    "scope.SAD.always_armed = True"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "50be2b5f-32e6-44fb-be9a-2a1a7ccf3e2e",
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "import scipy.stats\n",
    "from ipywidgets import interact, Layout\n",
    "from bokeh.io import push_notebook, output_notebook\n",
    "from bokeh.models import Span, Legend, LegendItem\n",
    "from bokeh.plotting import figure, show\n",
    "\n",
    "def update_plot():\n",
    "    temps.append(scope.XADC.temp)\n",
    "    S1.data_source.data['x'] = list(range(len(temps)))\n",
    "    S1.data_source.data['y'] = temps\n",
    "    push_notebook()\n",
    "\n",
    "output_notebook()\n",
    "S = figure(width=1800)\n",
    "temps = [scope.XADC.temp]\n",
    "xrange = [0]\n",
    "S1 = S.line(xrange, temps, line_color='red')\n",
    "show(S, notebook_handle=True)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "45810815-3a0e-4575-babe-2a9ad95bd623",
   "metadata": {},
   "outputs": [],
   "source": [
    "CHECK_INTERVAL = 60 # check slope every this many seconds\n",
    "TEMP_INTERVAL = 0.5 # measure temperature every this many seconds\n",
    "MIN_RUN_TIME = 300 # run for at least this many seconds\n",
    "print('Long test running, check plot above, temperature [celcius] as a function of time [seconds] to see the temperature rising... results checked every %d seconds' % CHECK_INTERVAL)\n",
    "while True:\n",
    "    measurements = int(CHECK_INTERVAL/TEMP_INTERVAL)\n",
    "    for i in range(measurements):\n",
    "        update_plot()\n",
    "        time.sleep(TEMP_INTERVAL)\n",
    "    slope = scipy.stats.linregress(range(measurements), temps[-measurements:]).slope\n",
    "    predicted_increase = slope*measurements\n",
    "    seconds_elapsed = TEMP_INTERVAL*len(temps)\n",
    "    if scope.XADC.status != 'good':\n",
    "        print('❌ XADC error detected! This should not happen. %s' % scope.XADC.errors)\n",
    "        scope.SAD.always_armed = False\n",
    "        break\n",
    "    if predicted_increase < 0.1 and seconds_elapsed > MIN_RUN_TIME: # stop when the slope predicts a < 0.1C increase over the next set of measurements\n",
    "        print('✅ Temperature looks stable! slope for last chunk of measurements: %0.5f; max temp: %3.1f; average max temp: %3.1f' % (slope, max(temps), np.average(temps[-measurements:])))\n",
    "        scope.SAD.always_armed = False\n",
    "        break\n",
    "    else:\n",
    "        print('🔥 Temperature still increasing; slope for last chunk of measurements: %0.5f; max temp: %3.1f; predicted increase: %3.1f' % (slope, max(temps), predicted_increase))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cc18d2de-5f98-4322-babc-28d705799d77",
   "metadata": {},
   "source": [
    "In the above we carefully monitored the temperature; now let's check the min/max voltages seen on the FPGA's VCC rails:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c75f9735-97ad-4ab9-8948-d4f7382667f9",
   "metadata": {},
   "outputs": [],
   "source": [
    "check_vcc_rails()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ab554a43-3603-4941-9268-221da82f6ac1",
   "metadata": {},
   "outputs": [],
   "source": [
    "def check_vcc_rails():\n",
    "    failed = False\n",
    "    for rail, nominal in zip(['vccint', 'vccaux', 'vccbram'],  [1.0, 1.8, 1.0]):\n",
    "        for worst,limit in zip(['min', 'max'], ['lower', 'upper']):\n",
    "            vseen = scope.XADC.get_vcc(rail, worst)\n",
    "            vlimit = scope.XADC.get_vcc_limit(rail, limit)\n",
    "            if worst == 'min':\n",
    "                vmargin = vseen - vlimit\n",
    "            else:\n",
    "                vmargin = vlimit - vseen\n",
    "            if vmargin > 0:\n",
    "                status = '✅ pass'\n",
    "            else:\n",
    "                status = '❌ FAIL!'\n",
    "                failed = True\n",
    "            print('%7s: nominal: %1.2f, %s seen: %1.2f, limit: %1.2f, margin: %1.2f   %s' % (rail, nominal, worst, vseen, vlimit, vmargin, status))\n",
    "    assert not failed\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f2bf5702-2206-4ebf-83d1-b4401df2402d",
   "metadata": {},
   "source": [
    "# Now let's gets the temperature even higher:\n",
    "\n",
    "We run a capture that will fail to trigger with a very long `scope.adc.timeout`. This *may* drive the temperature higher still.\n",
    "\n",
    "This part of the test takes 3 minutes.\n",
    "\n",
    "\"No trigger seen\" and \"Timeout happened during capture\" warnings are normal and expected here."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "66d05cbe-fac1-48e2-9e23-4dc801f25831",
   "metadata": {},
   "outputs": [],
   "source": [
    "scope.SAD.always_armed = True\n",
    "scope.SAD.interval_threshold = 2\n",
    "scope.adc.timeout = 30"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "b5fad874-9a8e-48ba-bf2b-059fc993ef66",
   "metadata": {},
   "outputs": [],
   "source": [
    "for i in range(6):\n",
    "    sadtrace = cw.capture_trace(scope, target, bytearray(16), bytearray(16), as_int=True)\n",
    "    assert scope.XADC.status == 'good'\n",
    "    print('Iteration %d/5: temp = %3.1f' % (i, scope.XADC.temp))\n",
    "print('✅ Success!')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "34bf982a-5f8a-4548-92d7-bc363ce3de8c",
   "metadata": {},
   "outputs": [],
   "source": [
    "scope.SAD.always_armed = False"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6823c859-889b-412b-b46b-d063e14c1038",
   "metadata": {},
   "source": [
    "Check the VCC rails again:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "37629471-2dfd-4da1-af67-56dd7745ae59",
   "metadata": {},
   "outputs": [],
   "source": [
    "check_vcc_rails()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3f157eb9-f723-402a-8a40-867c9d4797c7",
   "metadata": {},
   "source": [
    "# Run actual SAD captures at the maximum ADC frequency\n",
    "\n",
    "This requires the expected target and firmware to run out-of-the box."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "9f73e9f1-774d-4e41-b79a-434021c10eb7",
   "metadata": {},
   "outputs": [],
   "source": [
    "scope.adc.timeout = 2\n",
    "scope.adc.stream_mode = False\n",
    "scope.adc.samples = 98000\n",
    "scope.adc.presamples = 0\n",
    "scope.adc.segments = 1\n",
    "scope.gain.db = 20\n",
    "scope.trigger.module = 'basic'\n",
    "\n",
    "reftrace = cw.capture_trace(scope, target, bytearray(16), bytearray(16), as_int=True)\n",
    "\n",
    "assert not scope.adc.errors"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "d53f0554-1cba-4100-9b9e-4c8324fc6832",
   "metadata": {},
   "outputs": [],
   "source": [
    "if scope._is_husky_plus:\n",
    "    refstart = 55550\n",
    "else:\n",
    "    # different ADC frequency = different offset!\n",
    "    refstart = int(55550/25*20)\n",
    "scope.SAD.emode = True\n",
    "scope.SAD.reference = reftrace.wave[refstart:]\n",
    "\n",
    "from bokeh.plotting import figure, show\n",
    "from bokeh.io import output_notebook\n",
    "from bokeh.models import Span\n",
    "\n",
    "output_notebook()\n",
    "p = figure(width=1800, tools='pan, box_zoom, hover, reset, save')\n",
    "\n",
    "xrange = list(range(len(reftrace.wave)))\n",
    "p.line(xrange, reftrace.wave)\n",
    "p.renderers.extend([Span(location=refstart, dimension='height', line_color='black', line_width=2)])\n",
    "p.renderers.extend([Span(location=refstart+scope.SAD.sad_reference_length, dimension='height', line_color='black', line_width=2)])\n",
    "\n",
    "show(p)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c65b2eb9-e8ef-43e8-80c8-997aac3b9d63",
   "metadata": {},
   "outputs": [],
   "source": [
    "scope.trigger.module = 'SAD'\n",
    "\n",
    "scope.SAD.always_armed = False\n",
    "scope.SAD.multiple_triggers = True\n",
    "scope.SAD.interval_threshold = 10\n",
    "scope.SAD.threshold = 10\n",
    "\n",
    "scope.adc.stream_mode = False\n",
    "scope.adc.samples = 3000\n",
    "scope.adc.presamples = scope.SAD.sad_reference_length + scope.SAD.latency\n",
    "scope.adc.segments = 10"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5854b57c-058b-4383-9606-a7fbfd59cf01",
   "metadata": {},
   "source": [
    "Try once..."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "4d7a3f9d-8cd0-4466-91ef-8c1cbbb1834a",
   "metadata": {},
   "outputs": [],
   "source": [
    "scope.SAD.reference = reftrace.wave[refstart:]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "560143c5-9dc6-4325-a664-6a33369a0372",
   "metadata": {},
   "outputs": [],
   "source": [
    "sadtrace = cw.capture_trace(scope, target, bytearray(16), bytearray(16), as_int=True)\n",
    "assert scope.SAD.num_triggers_seen == 10\n",
    "assert scope.XADC.status == 'good'\n",
    "assert not scope.adc.errors"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ee357832-b995-472b-8ce7-43157021d13b",
   "metadata": {},
   "source": [
    "Then a few more times:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "5ea34bcd-2374-435d-bbbe-e5b1bd503568",
   "metadata": {},
   "outputs": [],
   "source": [
    "scope.SAD.always_armed = False"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "3ddda76c-c4cd-42b1-99ed-61873d6fbf6d",
   "metadata": {},
   "outputs": [],
   "source": [
    "from tqdm.notebook import tnrange\n",
    "for i in tnrange(100):\n",
    "    sadtrace = cw.capture_trace(scope, target, bytearray(16), bytearray(16), as_int=True)\n",
    "    assert scope.SAD.num_triggers_seen == 10\n",
    "    assert scope.XADC.status == 'good'\n",
    "    assert not scope.adc.errors"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a028abde-b2fd-4e5c-9b11-4fddcb550507",
   "metadata": {},
   "source": [
    "Visual check:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "d9612644-8f0e-4970-8d81-221231efab42",
   "metadata": {},
   "outputs": [],
   "source": [
    "from bokeh.palettes import inferno\n",
    "from bokeh.plotting import figure, show\n",
    "from bokeh.resources import INLINE\n",
    "from bokeh.io import output_notebook\n",
    "from bokeh.models import Span, Legend, LegendItem\n",
    "import itertools\n",
    "\n",
    "SAMPLES = scope.SAD.sad_reference_length\n",
    "\n",
    "numplots = scope.adc.segments\n",
    "xrange = list(range(SAMPLES))\n",
    "p = figure(width=1800)\n",
    "colors = itertools.cycle(inferno(numplots))\n",
    "for i in range(numplots):\n",
    "    offset = i*scope.adc.samples\n",
    "    p.line(xrange, sadtrace.wave[offset:offset+SAMPLES], color=next(colors))\n",
    "\n",
    "p.line(xrange, scope.SAD.reference[:SAMPLES], line_color='grey', line_width=3, line_dash='dotted')\n",
    "show(p)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e96b624f-bd5f-4e34-baa3-ab86fb20c25c",
   "metadata": {},
   "outputs": [],
   "source": [
    "assert scope.XADC.status == 'good'"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "44772eae-93f8-4c48-a195-1849a01b2aab",
   "metadata": {},
   "source": [
    "This in particular can stress the VCC rails:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "52e7e0ef-4c27-4902-b04b-978b5e478ddd",
   "metadata": {},
   "outputs": [],
   "source": [
    "check_vcc_rails()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "acfe70ed-8c02-4573-8408-9e162ebf31e2",
   "metadata": {},
   "source": [
    "**If everything until here passed, the test is done!**\n",
    "\n",
    "If SAD triggering doesn't work as expected, try tweaking the parameters with the help of `SADEXplorer`. (There is no need to run this if everything above passed without errors.)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "dc66ba25-700e-450a-81cb-e5d28ba5a0ac",
   "metadata": {
    "tags": []
   },
   "outputs": [],
   "source": [
    "#explorer = cw.SADExplorer(scope, target, reftrace.wave, refstart, max_segments=10)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e6f19c51-a153-461a-8704-bcc6baeaca4b",
   "metadata": {},
   "source": [
    "Otherwise, turn off all the hot stuff:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "19f07b27-4287-485e-a40d-f8faf54322f1",
   "metadata": {},
   "outputs": [],
   "source": [
    "scope.trigger.module = 'basic'\n",
    "scope.clock.adc_mul = 1"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "6a58e93a-24c3-4cd5-80ef-82a6bbc298ae",
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python (venv39)",
   "language": "python",
   "name": "venv39"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.9.7"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
